package org.noip.milhous;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.noip.milhous.annotations.DatabaseMapping;
import org.noip.milhous.annotations.dbVar;
import org.noip.milhous.annotations.foriegnKey;
import org.noip.milhous.annotations.primarykey;
import org.noip.milhous.annotations.transientVar;

public class DatastoreObject<Type>
{

	private static final String JAVA_LANG_OBJECT = "java.lang.Object";

	protected final Class<?> tableClass;

	protected final Map<String, FieldData> mappingJavaToDB;

	protected String tableName = null;

	protected String keyName = null;

	protected final Map<String, FieldData> forignTableToKey;

	protected static Boolean debug = false;

	@SuppressWarnings("rawtypes")
	public DatastoreObject(Class theClass, Type obj)
		{
			tableClass = theClass;
			mappingJavaToDB = new HashMap<String, FieldData>();
			if (obj instanceof TableNameMapping)
				{
					tableName = ((TableNameMapping) obj).getRealTableName();
				}
			else
				{
					Set<Method> methods = getListOfMethods(theClass);
					for (Method m : methods)
						{
							if (m.getAnnotation(org.noip.milhous.annotations.tableName.class) != null)
								{
									try
										{
											tableName = m.invoke(obj).toString();
											break;
										}
									catch (Exception e)
										{
											System.out.println(theClass.toString() + "--" + theClass.getCanonicalName() + " " + obj.toString());
											e.printStackTrace();
										}
								}
						}
					if (tableName == null)
						{
							tableName = tableClass.getSimpleName();
						}
				}
			forignTableToKey = new HashMap<String, FieldData>();
			createMapping(obj);
		}

	/**
	 * Set the table name in the database in this table
	 * 
	 * @param tableName
	 *           the name to use for database calls
	 */
	public void setTableName(String tableName)
		{
			this.tableName = tableName;
		}

	/**
	 * Get the real name of the table
	 * 
	 * @return the name of the table in the database
	 */
	public String getTableName()
		{
			return this.tableName;
		}

	public Set<Field> getListOfFields()
		{
			Set<Field> result = getListOfFields(tableClass);
			return result;
		}

	/**
	 * Get the foreign key for a given table
	 * 
	 * @param tableToLookup
	 *           the table to check for a foreign key
	 * @return the mapping
	 */
	public FieldData getForignKey(String tableToLookup)
		{
			return forignTableToKey.get(tableToLookup);
		}

	@SuppressWarnings("rawtypes")
	public static Set<Field> getListOfFields(Class myClass)
		{
			Set<Field> result = new HashSet<Field>();
			Field[] fields = myClass.getDeclaredFields();
			for (Field f : fields)
				{
					if (f.getAnnotation(dbVar.class) != null)
						{
							result.add(f);
						}
				}
			if (!myClass.getCanonicalName().equals(JAVA_LANG_OBJECT))
				{
					result.addAll(getListOfFields(myClass.getSuperclass()));
				}
			return result;
		}

	@SuppressWarnings("rawtypes")
	public static Set<Method> getListOfMethods(Class myClass)
		{
			Set<Method> result = new HashSet<Method>();
			Method[] fields = myClass.getDeclaredMethods();
			for (Method m : fields)
				{
					result.add(m);
				}
			if (!myClass.getCanonicalName().equals(JAVA_LANG_OBJECT))
				{
					result.addAll(getListOfMethods(myClass.getSuperclass()));
				}
			return result;
		}

	@SuppressWarnings("rawtypes")
	public static Set<Constructor> getListOfConstructors(Class myClass)
		{
			Set<Constructor> result = new HashSet<Constructor>();
			Constructor[] fields = myClass.getDeclaredConstructors();
			for (Constructor m : fields)
				{
					result.add(m);
				}
			return result;
		}

	/**
	 * create a mapping that maps the java names to the database names
	 * 
	 * @param tableObject
	 *           the object to create the mapping for
	 */
	protected void createMapping(Type tableObject)
		{
			for (Field f : getListOfFields())
				{
					if (f.getAnnotation(transientVar.class) == null)
						{
							String fieldName = f.getName();
							Class<?> e = f.getType();
							FIELDTYPES type = FIELDTYPES.UserDefined;
							type = type.getFIELDTYPE(e.getCanonicalName());
							FieldData data = new FieldData();

							data.databasename = databaseName(tableObject, fieldName);
							data.javaname = fieldName;
							data.type = type;
							setKeys(f, data);
							mappingJavaToDB.put(fieldName, data);
							if (debug)
								System.out.println(fieldName + " | " + data + " ");
						}
					else
						{
							System.out.println("skipping " + f.getName());
						}
				}
		}

	public String databaseName(Type tableObject, String fieldName)
		{
			String result = null;
			if (tableObject instanceof CustomFieldMapping)
				{
					result = ((CustomFieldMapping) tableObject).getJavaToDbMapping(fieldName);
				}
			else
				{
					Set<Method> methods = getListOfMethods(tableClass);
					for (Method m : methods)
						{
							if (m.getAnnotation(DatabaseMapping.class) != null)
								{
									try
										{
											Object temp = m.invoke(tableObject, fieldName);
											if (temp != null)
												{
													result = temp.toString();
												}
											else
												{
													result = null;
												}
										}
									catch (Exception e)
										{
											System.err.println(tableObject.getClass().getCanonicalName() + " + " + fieldName + " " + m.getName());
											e.printStackTrace();
										}
								}
						}
					if (result == null)
						{
							result = fieldName;
						}
				}
			return result;
		}

	/**
	 * Recursively look for a field
	 * 
	 * @param currentClass
	 * @param field
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public Field getField(Class currentClass, String field)
		{
			Field result = null;
			try
				{
					result = currentClass.getDeclaredField(field);
				}
			catch (SecurityException e)
				{
					e.printStackTrace();
				}
			catch (NoSuchFieldException e)
				{
					result = getField(currentClass.getSuperclass(), field);
				}
			if (result.isAccessible() == false)
				{
					result.setAccessible(true);
				}
			return result;
		}

	/**
	 * Poke in the object and set the desired field
	 * 
	 * @param o
	 *           object to poke
	 * @param fieldName
	 *           field to poke at
	 * @param value
	 *           the new value
	 */
	protected void setData(Object o, String fieldName, Object value)
		{
			Field privateStringField = null;
			try
				{
					privateStringField = getField(o.getClass(), fieldName);
					privateStringField.set(o, value);
				}
			catch (Exception e)
				{
					// try
					// {
					// Object op = Class.forName(privateStringField.getClass().getCanonicalName()).newInstance();
					// Set<Field> fields = getListOfFields(privateStringField.getClass());
					// for (Field f : fields)
					// {
					//
					// }
					// }
					// catch (Exception e1)
					// {
					// // TODO Auto-generated catch block
					// e1.printStackTrace();
					// }

					e.printStackTrace();
				}
		}

	/**
	 * Set the {@link primarykey} and {@link foriegnKey} keys for the table
	 * 
	 * @param f
	 *           the field to check
	 * @param data
	 *           the data mapping the class name to the field name
	 */
	private void setKeys(Field f, FieldData data)
		{
			try
				{
					if (f.getAnnotation(primarykey.class) != null)
						{
							keyName = f.getName();
						}
					else if (f.getAnnotation(foriegnKey.class) != null)
						{
							foriegnKey fkValue = f.getAnnotation(foriegnKey.class);
							forignTableToKey.put(fkValue.table(), data);
						}
				}
			catch (Exception e)
				{
					e.printStackTrace();
				}
		}

	public String getTableKey()
		{
			return keyName;
		}

	public FieldData getForiegnKey(String table)
		{
			return forignTableToKey.get(table);
		}

	/**
	 * Peek into the object to find the value of the fields
	 * 
	 * @param o
	 *           the class object to peek into
	 * @param field
	 *           the field to look for
	 * @return the value from the object
	 */
	protected Object getData(Object o, String field)
		{
			Field privateStringField;
			Object result = null;
			try
				{
					privateStringField = getField(o.getClass(), field);
					result = privateStringField.get(o);
				}
			catch (Exception e)
				{
					for (Field f : o.getClass().getFields())
						{
							System.err.println("--" + f.getName());
						}
					e.printStackTrace();
				}
			return result;
		}

	public static void setDebug(Boolean debug)
		{
			DatastoreObject.debug = debug;
		}

}